﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using log4net.Appender;
using log4net.Core;
using log4net.Util;

namespace Infrastructure.Logging.Appenders
{
    public abstract class DatabaseAppenderBase : BufferingAppenderSkeleton
    {
        /// <summary>
        /// The database connection string used to connect to the database.
        /// </summary>
        private string _connectionString;

        /// <summary>
        /// The stored procedure name to insert logging events into the database.
        /// </summary>
        protected string _storedProcedure;

        /// <summary>
        /// the database connection string name in config section that is used to connect to the database.
        /// </summary>
        private string _connectionStringName;

        /// <summary>
        /// The SQL server DB connection used to write log.
        /// </summary>
        protected SqlConnection _connection;

        /// <summary>
        /// the IP of the server that the logger is running on
        /// </summary>
        protected string _serverIp;

        /// <summary>
        /// Gets or sets the database connection string that is used to connect to 
        /// the database.
        /// </summary>
        public string ConnectionString
        {
            get { return _connectionString; }
            set { _connectionString = value; }
        }

        /// <summary>
        /// Gets or sets the database connection string name that is used to connect to 
        /// the database. get the connection string from <see cref="ConfigurationManager.ConnectionStrings"/> section of config.
        /// </summary>
        public string ConnectionStringName
        {
            get { return _connectionStringName; }
            set { _connectionStringName = value; }
        }

        /// <summary>
        /// Gets or sets the stored procedure name that is used to insert logging events
        /// into the database.
        /// </summary>
        public string StoredProcedure
        {
            get { return _storedProcedure; }
            set { _storedProcedure = value; }
        }

        /// <summary>
        /// Initialize the appender based on the options set
        /// </summary>
        /// <remarks>
        /// <para>
        /// This is part of the <see cref="IOptionHandler"/> delayed object
        /// activation scheme. The <see cref="ActivateOptions"/> method must 
        /// be called on this object after the configuration properties have
        /// been set. Until <see cref="ActivateOptions"/> is called this
        /// object is in an undefined state and must not be used. 
        /// </para>
        /// <para>
        /// If any of the configuration properties are modified then 
        /// <see cref="ActivateOptions"/> must be called again.
        /// </para>
        /// </remarks>
        override public void ActivateOptions()
        {
            var serverIp = Dns.GetHostAddresses(Dns.GetHostName()).FirstOrDefault(address => address.AddressFamily != AddressFamily.InterNetworkV6);
            if (serverIp != null)
                _serverIp = serverIp.ToString();

            if (!string.IsNullOrEmpty(_connectionStringName))
            {
                var cs = ConfigurationManager.ConnectionStrings[_connectionStringName];
                if (cs != null)
                {
                    _connectionString = cs.ConnectionString;
                }
                else
                {
                    LogLog.Error("ErrorDataBaseAppender: failed to find connection string by name [" + _connectionStringName + "]");
                }
            }

            base.ActivateOptions();
            InitializeDatabaseConnection();
        }

        /// <summary>
        /// Inserts the events into the database.
        /// </summary>
        /// <param name="events">The events to insert into the database.</param>
        /// <remarks>
        /// <para>
        /// Insert all the events specified in the <paramref name="events"/>
        /// array into the database.
        /// </para>
        /// </remarks>
        protected override void SendBuffer(LoggingEvent[] events)
        {
            ThreadPool.QueueUserWorkItem( SendBufferAsync, events );
        }

        /// <summary>
        /// Send the logging events on a thread pool thread and not on working thread.
        /// </summary>
        /// <param name="state">list of all logging events to send</param>
        private void SendBufferAsync( object state )
        {
            try
            {
                var events = (IEnumerable<LoggingEvent>)state;
                foreach (var logEvent in events)
                {
                    Exception ex = null;
                    for (int i = 0; i < 3; i++)
                    {
                        try
                        {
                            if( _connectionString == null || _connection == null ||
                                ( _connection.State != ConnectionState.Open && _connection.State != ConnectionState.Connecting ) )
                            {
                                LogLog.Debug( "ErrorDataBaseAppender: Attempting to reconnect to database. Current Connection State: " + ( ( _connection == null ) ? "<null>" : _connection.State.ToString() ) );
                                InitializeDatabaseConnection();
                            }

                            // Check that the connection exists and is open
                            if( _connection != null && _connection.State == ConnectionState.Open )
                            {
                                using( var command = GetCommand( logEvent ) )
                                {
                                    if( command != null )
                                    {
                                        command.ExecuteNonQuery();
                                        ex = null;
                                        break;
                                    }
                                }
                            }
                        }
                        catch (Exception e)
                        {
                            ex = e;
                            LogLog.Warn("Failed to write event log to DB (sleep for 100ms)", e);
                            Thread.Sleep(100);
                        }
                    }
                    if( ex != null )
                    {
                        LogLog.Error("All retries failed to write event to DB!",ex);
                    }
                }
            }
            catch( Exception ex )
            {
                LogLog.Error( "Exception in SendBufferAsync of ErrorDataBaseAppender", ex );
            }
        }

        /// <summary>
        /// Connects to the database.
        /// </summary>		
        private void InitializeDatabaseConnection()
        {
            try
            {
                // Cleanup any existing connection
                if (_connection != null)
                {
                    try
                    {
                        _connection.Close();
                    }
                    catch (Exception ex)
                    {
                        LogLog.Warn("ErrorDataBaseAppender: Exception while disposing cached connection object", ex);
                    }
                    _connection = null;
                }

                // Create the connection object, Set the connection string
                _connection = new SqlConnection(_connectionString);
                _connection.Open();
            }
            catch (Exception e)
            {
                ErrorHandler.Error("Could not open database connection [" + _connectionString + "]", e);
                _connection = null;
            }
        }

        /// <summary>
        /// Prepares the database command and initialize the parameters.
        /// </summary>
        protected abstract SqlCommand GetCommand(LoggingEvent logEvent);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="logEvent"></param>
        /// <returns></returns>
        protected static string GetPropertiesDataString(LoggingEvent logEvent)
        {
            var sb = new StringBuilder();
            foreach (DictionaryEntry property in logEvent.Properties)
            {
                string value;
                try
                {
                    value = property.Value.ToString();
                }
                catch( Exception ex )
                {
                    value = "Exception in property value: "+ ex.Message;
                }

                sb.AppendFormat( "{0} = {1}", property.Key, value );
                sb.AppendLine();
            }
            return sb.ToString();
        }
    }
}